package com.leyou.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exceptions.LyException;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.JsonUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.dto.GoodsDTO;
import com.leyou.dto.SearchRequest;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.*;
import com.leyou.mapper.GoodsRepository;
import com.leyou.pojo.Goods;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author: guojy
 * @date: 2020/3/6 19:42
 * @Description: 通过查询 spu  sku  构建Goods
 * @version: 1.0
 */
@Service
public class SearchService {
    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    // item服务提供的接口
    private ItemClient itemClient;

    // TODO 构建goods
    public Goods buildGoods(SpuDTO spuDTO) {
        //========组织all字段 用来进行全文检索的字段，里面包含标题、商品分类、品牌等信息========
        // 1.1  查询分类,并获取每个分类的名字，用【,】分隔
        List<CategoryDTO> categoryDTOList = itemClient.findCategoryByIds(spuDTO.getCategoryIds());
        String categoryName = categoryDTOList.stream().map(CategoryDTO::getName).collect(Collectors.joining(","));
        // 1.2 查询品牌信息
        BrandDTO brandById = itemClient.findBrandById(spuDTO.getBrandId());
        String brandName = brandById.getName();
        // 1.3 拼接
        String all = spuDTO.getName() + categoryName + brandName;


        //========2、组织skus，只需要sku的部分数据，image，price，id，title，用map封装,并转化为json========
        List<SkuDTO> skuDTOList = itemClient.findSkusBySpuId(spuDTO.getId());
        // 2.1 构建map
        List<Map<String, Object>> skuMap = new ArrayList<>();
        for (SkuDTO skuDTO : skuDTOList) {
            Map<String, Object> map = new HashMap<>();
            map.put("id", skuDTO.getId());
            map.put("title", skuDTO.getTitle());
            map.put("price", skuDTO.getPrice());
            // 这里要注意，查询到结果的时候，是spu，每个sku只有一张照片，所以要分隔，取第一张
            map.put("image", StringUtils.substringBefore(skuDTO.getImages(), ","));
            skuMap.add(map);
        }
        // 2.2 转化json字符串
        String skus = JsonUtils.toString(skuMap);

        // ========3、获取当前 sku的价格，存入set========
        Set<Long> skuPrices = skuDTOList.stream().map(SkuDTO::getPrice).collect(Collectors.toSet());


        // ========4、获取规格参数信息，构建 specs map集合========
        /**
         * apecs应当存储的是  {"品牌":"**","上市年份":"**","机身颜色":["白","黑","红"]}
         *
         * spudetail表示中有两个  generic_spec(通用规格)  special_spec(特殊规格)
         *  generic_spec: {"1":"华为","2":"G9青春版（全网通版）"}
         *  special_spec: {"4":["白色","金色","玫瑰金"],"12":["3GB"],"13":["16GB"]}
         *
         * 发现  这两个字段，key都是 规格的id，所以就是要根据这两个字段，来构建 specs
         */
        // 4.1查询所有可以被用来搜索的规格参数  关键字,当前分类id， searching字段
        List<SpecParamDTO> specParamList = itemClient.findSpecParam(null, spuDTO.getCid3(), true);
        // 4.1 查询 spudetail表中  generic_spec字段，并构建成map，方便根据 规格参数id，查找值
        SpuDetailDTO spuDetail = itemClient.findSpuDetailBySpuId(spuDTO.getId());
        // 4.2 因为 通用规格参数和特有规格参数，保存的格式不一样，所以分开构建map
        String genericSpec = spuDetail.getGenericSpec();
        Map<Long, String> genericSpecMap = JsonUtils.toMap(genericSpec, Long.class, String.class);
        // 由于 specialSpec 数据格式比较特殊，value值是一个list，所以不能直接用第一种方式，因为List.class,会无法正确得到CLass
        String specialSpec = spuDetail.getSpecialSpec();
        Map<Long, List<String>> specialSpecMap = JsonUtils.nativeRead(specialSpec, new TypeReference<Map<Long, List<String>>>() {
        });
        // 4.3 构建specs  循环specParamList，获得其name和id，通过id ，从上边的两个map中取值

        Map<String, Object> specsMap = new HashMap<>();
        for (SpecParamDTO specParamDTO : specParamList) {
            // 规格参数 name，作为key值
            String key = specParamDTO.getName();
            // 规格参数值，作为value
            Object value = null;
            // 判断当前 specParamDTO 是否是 通用规格参数  关键字 generic = true
            if (specParamDTO.getGeneric()) {
                value = genericSpecMap.get(specParamDTO.getId());
            } else {
                value = specialSpecMap.get(specParamDTO.getId());
            }

            // 判断当前 规格参数是否是数字，如果是数字需要特殊处理，比如按照价格搜索的时候，【100~200，200~500，其他】
            if (specParamDTO.getIsNumeric()) {
                // 是数字类型，分段
                value = chooseSegment(value, specParamDTO);
            }
            specsMap.put(key, value);
        }


        // 组织数据
        Goods goods = new Goods();
        // 从spu对象中拷贝与goods对象中属性名一致的属性
        goods.setBrandId(spuDTO.getBrandId());
        goods.setCategoryId(spuDTO.getCid3());
        goods.setId(spuDTO.getId());
        goods.setSubTitle(spuDTO.getSubTitle());
        goods.setCreateTime(spuDTO.getCreateTime().getTime());
        goods.setSkus(skus);// spu下的所有sku的JSON数组
        goods.setSpecs(specsMap); // 当前spu的规格参数
        goods.setPrice(skuPrices); // 当前spu下所有sku的价格的集合
        goods.setAll(all);// 商品相关搜索信息的拼接：标题、分类、品牌、规格信息等
        return goods;
    }

    //TODO 因为过滤参数中有一类比较特殊，就是数值区间，所以 在
    private String chooseSegment(Object value, SpecParamDTO p) {
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }

    @Autowired
    private ElasticsearchTemplate ESTemplate;

    public QueryBuilder buildBasicQuery(SearchRequest searchRequest) {

        //1 获取关键词
        String keyWord = searchRequest.getKey();
        //1.1 非空判断
        if (StringUtils.isBlank(keyWord)) {
            // 无效的请求参数
            throw new LyException(ExceptionEnum.INVALID_PARAM_ERROR);
        }

        // 创建一个基本的  搜索构建器，用BOOl查询，组合match查询和之后的过滤查询
        // 1.构建布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //2. 构建基本的 match查询
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", keyWord).operator(Operator.AND));
        // 3.构建过滤条件，根据关键字搜索之后，可以根据品牌等信息继续过滤
        Map<String, String> filter = searchRequest.getFilter();
        // 4.分析map
        if (!CollectionUtils.isEmpty(filter)) {
            // 4.1遍历 map，构建过滤条件
            for (Map.Entry<String, String> entry : filter.entrySet()) {
                //前台传递的参数  filter: {品牌: 2032, CPU品牌: "骁龙（Snapdragon)"}
                // 获取 key值，创建要 term查询的  字段名
                String key = entry.getKey();
                if ("分类".equals(key)) {
                    // 如果 是分类的话，ES中的数据字段是 categoryId
                    key = "categoryId";
                } else if ("品牌".equals(key)) {
                    // 如果 是品牌的话，ES中的数据字段是 brandId
                    key = "brandId";
                } else {
                    // 如果是规格参数的话，ES中查询应该是  specs.CPU品牌
                    key = "specs." + key;
                }
                // 过滤搜索的关键字
                String value = entry.getValue();
                // 添加过滤条件
                boolQueryBuilder.filter(QueryBuilders.termQuery(key,value));
            }
        }
        return boolQueryBuilder;
    }

    /**
     * 从 ES中 match查询 关键字
     *
     * @param searchRequest
     * @return
     */
    public PageResult<GoodsDTO> search(SearchRequest searchRequest) {
//        //1 获取关键词
//        String key = searchRequest.getKey();
//        //1.1 非空判断
//        if (StringUtils.isBlank(key)) {
//            // 无效的请求参数
//            throw new LyException(ExceptionEnum.INVALID_PARAM_ERROR);
//        }
        //2 创建原生 构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //3 设置过滤字段
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));
        //4 设置搜索关键字
        queryBuilder.withQuery(buildBasicQuery(searchRequest));

        //5 设置分页
        // 前端传递的第一页，在数据ES中应该是 从 0  开始 所以需要－1
        int page = searchRequest.getPage() - 1;
        queryBuilder.withPageable(PageRequest.of(page, searchRequest.getSize()));
        //6 使用ES客户端发起搜索
        AggregatedPage<Goods> goods = ESTemplate.queryForPage(queryBuilder.build(), Goods.class);
        //7 处理查询结果
        // 总条数
        long total = goods.getTotalElements();
        // 总页数
        long totalPages = goods.getTotalPages();
        List<Goods> goodsList = goods.getContent();
        List<GoodsDTO> goodsDTOS = BeanHelper.copyWithCollection(goodsList, GoodsDTO.class);
        //8 封装返回结果
        PageResult<GoodsDTO> pageResult = new PageResult<>(total, totalPages, goodsDTOS);
        return pageResult;
    }

    /**
     * 查询 查询过滤项，聚合查询
     *
     * @param searchRequest
     * @return
     */
    public Map<String, List<?>> filter(SearchRequest searchRequest) {
        // 1.创建过滤项集合
        Map<String, List<?>> filterMap = new LinkedHashMap<>();
        // 2 创建构造器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 2.1 设置查询条件
        queryBuilder.withQuery(buildBasicQuery(searchRequest));
        // 2.2设置要显示的字段,不用显示字段
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));
        // 2.3设置显示的数据显示的条数
        queryBuilder.withPageable(PageRequest.of(0, 1));

        // 3 进行聚合查询
        // 3.1.分类聚合
        String categoryAgg = "categoryAgg";
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
        // 3.2 品牌聚合
        String brandAgg = "brandAgg";
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));

        // 4 查询并解析结果
        AggregatedPage<Goods> result = ESTemplate.queryForPage(queryBuilder.build(), Goods.class);
        // 4.1 获取聚合结果
        Aggregations aggregations = result.getAggregations();
        // 4.2 获取分类聚合
        /**
         * LongTerms:表示 聚合方式Terms + 聚合字段的类型long,
         * 定位到具体的聚合类型，可以方便的使用国内部的各种方法
         */

        LongTerms cAgg = aggregations.get(categoryAgg);
        // 4.2.1 处理分类聚合，将结果放入  Map<String, List<?>> filterMap
        List<Long> cids = handleCategoryAgg(cAgg, filterMap);
        // 4.3 获取品牌聚合
        LongTerms bAgg = aggregations.get(brandAgg);
        handleBrandAgg(bAgg, filterMap);

        // 判断分类结果，是否只有一条，如果只有一条才进行规格参数聚合
        if (cids != null && cids.size() == 1) {
            // 发起一次新的查询，将规格参数的聚合结果，添加到Map中
            handlerSpecAgg(filterMap, searchRequest, cids.get(0));
        }
        return filterMap;
    }

    /**
     * 发起一次新的查询，将规格参数的聚合结果，添加到Map中
     *
     * @param filterMap
     * @param searchRequest
     * @param cid
     */
    private void handlerSpecAgg(Map<String, List<?>> filterMap, SearchRequest searchRequest, Long cid) {
        /**
         * 由于以下逻辑只是为了，聚合规格参数，所以就另外新建一个 新的构造器
         */
        // 2 创建构造器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 2.1 设置查询条件
        queryBuilder.withQuery(buildBasicQuery(searchRequest));
        // 2.2设置要显示的字段,不用显示字段
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));
        // 2.3设置显示的数据显示的条数
        queryBuilder.withPageable(PageRequest.of(0, 1));

        // 3 进行聚合查询,规格参数聚合
        // 3.1根据 cid查询具体的规格参数，为了获取规格参数的名字，将来作为聚合的
        List<SpecParamDTO> specParam = itemClient.findSpecParam(null, cid, true);
        for (SpecParamDTO specParamDTO : specParam) {
            // 获取规格参数名称,由于作为聚合桶的名字和聚合的条件
            String name = specParamDTO.getName();
            // 根据 规格参数名称 进行聚合查询,此时要注意聚合的字段是什么，ES语法中，由于spec是个map，所以要具体使用spec.key值进行聚合
            // 所以要 "field": "specs.CPU品牌"
            /**
             *  "CPU品牌":{
             *       "terms": {
             *         "field": "specs.CPU品牌"
             *       }
             *     }
             */
            queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs." + name));
        }
        // 4 执行查询
        AggregatedPage<Goods> goods = ESTemplate.queryForPage(queryBuilder.build(), Goods.class);
        // 5 处理结果
        Aggregations aggregations = goods.getAggregations();
        for (SpecParamDTO specParamDTO : specParam) {
            // 获取规格参数名称
            String name = specParamDTO.getName();
            StringTerms specAgg = aggregations.get(name);
            // 处理  规格参数结果，
            List<String> collect = specAgg.getBuckets().stream().
                    map(StringTerms.Bucket::getKeyAsString).
                    filter(StringUtils::isNotBlank). // 用于过滤非空字段
                    collect(Collectors.toList());
            // 将规格参数放到 Map中，返回前端
            filterMap.put(name, collect);
        }
    }

    /**
     * 处理品牌聚合结果，将结果放入  Map<String, List<?>> filterMap
     *
     * @param bAgg
     * @param filterMap
     */
    private void handleBrandAgg(LongTerms bAgg, Map<String, List<?>> filterMap) {
        // 方法一：普通方式，遍历取值
        List<LongTerms.Bucket> buckets = bAgg.getBuckets();
        List<Long> bIds = new ArrayList<>();
        // 遍历桶中的内容，获取桶内数据，转换成  id集合
        for (LongTerms.Bucket bucket : buckets) {
            // 获取 key值，Number 是所有数值的顶级父类
            Number keyAsNumber = bucket.getKeyAsNumber();
            // 转化成 Long类型
            long bId = keyAsNumber.longValue();
            bIds.add(bId);
        }
        // 方法二：Stream 流的方式，使用map转化
/*        List<Long> bIds = bAgg.getBuckets().stream().
                map(LongTerms.Bucket::getKeyAsNumber).
                map(Number::longValue).
                collect(Collectors.toList());*/

        // 非空判断
        if (CollectionUtils.isEmpty(bIds)) {
            return;
        }
        List<BrandDTO> brandsByIds = itemClient.findBrandsByIds(bIds);
        if (CollectionUtils.isEmpty(brandsByIds)) {
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }
        filterMap.put("品牌", brandsByIds);
    }

    /**
     * 处理分类聚合，将结果放入  Map<String, List<?>> filterMap
     *
     * @param cAgg
     * @param filterMap
     */
    private List<Long> handleCategoryAgg(LongTerms cAgg, Map<String, List<?>> filterMap) {
        // 使用 cAgg  构建  CategoryId集合
        // 方式一：使用Lambda 和 Stream流，构建数据，省略写法
        /*List<Long> idList = terms.getBuckets().stream()
                .map(LongTerms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());*/
        // 方式二:使用Stream流，未省略写法
        List<Long> cids = cAgg.getBuckets().stream().map(new Function<LongTerms.Bucket, Number>() {
            @Override
            public Number apply(LongTerms.Bucket bucket) {
                Number keyAsNumber = bucket.getKeyAsNumber();
                return keyAsNumber;
            }
        }).map(new Function<Number, Long>() {
            @Override
            public Long apply(Number number) {
                long l = number.longValue();
                return l;
            }
        }).collect(Collectors.toList());

        // 非空判断
        if (CollectionUtils.isEmpty(cids)) {
            return null;
        }
        // 根据分类id集合  查询分类名字
        List<CategoryDTO> categoryDTOList = itemClient.findCategoryByIds(cids);
        if (CollectionUtils.isEmpty(categoryDTOList)) {
            throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
        }
        filterMap.put("分类", categoryDTOList);
        return cids;
    }
}
